[Swift] SwiftのmapとflatMapの動作を追ってみる
はじめに
こんにちは!
モバイルアプリサービス部の田中孝明です。
はじめに言っておきますと、iOS 10やSwift 3.0とはあまり関係無い内容です。
Swift 3.0に移行する上で、従来の機能をおさらいしておこうという内容のブログになります。
mapとflatMap
Swift 1.2から追加されたメソッドです。
Collection(Array)やOptionに備わっています。
このメソッドに共通する事は、「中身があるかもしれない」「なにかしらのオブジェクトの配列」などの状態を持つオブジェクトに対して、中身を操作する手段を提供しています。
Collectionに対するそれぞれの挙動
おそらくは一番馴染み深い方法ではないでしょうか。
mapはCollectionの中身に対して、処理した内容を愚直に「適用」していきます。
例えば以下のようなEnumがあったと仮定します。
enum Type: String { case grass = "grass" case water = "water" case fire = "fire" case electric = "electric" }
map
APIのレスポンス等で文字列で返ってきた値をパースする際、定義されているEnumに変換する処理を行ってみます。
これをmap
でまず行ってみましょう。
let typeStrings = ["grass", "water", "fire", "electric"] let types = typeStrings.map { Type(rawValue: $0) }
愚直に処理内容が適用されるので結果はType
のOptional
のArrayになります。
[Optional(Type.grass), Optional(Type.water), Optional(Type.fire), Optional(Type.electric)]
flatMap
次にflatMap
で行ってみます。
let typeStrings = ["grass", "water", "fire", "electric"] let types = typeStrings.flatMap { Type(rawValue: $0) }
すると結果をアンラップして返す処理が働きます。
[Type.grass, Type.water, Type.fire, Type.electric]
map
Collectionのmap
は愚直に適用していくので、nilが混じることもあります。
let typeStrings = ["grass", "water", "fire", "ice"] let types = typeStrings.map { Type(rawValue: $0) } // iceはTypeに定義されていないので、生成に失敗する [Optional(Type.grass), Optional(Type.water), Optional(Type.fire), nil]
flatMap
対してCollectionのflatMap
はnilの結果を無視しますので、mapでnilが混ざるようなケースを除外する事もできます。
let typeStrings = ["grass", "water", "fire", "ice"] let types = typeStrings.flatMap { Type(rawValue: $0) }
// IceはTypeに定義されていないので、生成に失敗する [Type.grass, Type.water, Type.fire]
双方それぞれ使いどころがあると思います。
Collection同士の処理に対するそれぞれの挙動
map
ではCollection(Array)同士のmap
処理の結果を見てみましょう。
let types1 = ["electric", "electric", "fire"] let types2 = ["water", "ice"] let types3 = ["fire", "grass", "rock", "grass"] let types = [types1, types2, types3].map { $0 }
これも愚直に適用処理が働きますので、以下のようなArrayが入れ子になる構造になります。
[["electric", "electric", "fire"], ["water", "ice"], ["fire", "grass", "rock", "grass"]]
flatMap
それではflatMap
の場合はどうでしょうか。
let types1 = ["electric", "electric", "fire"] let types2 = ["water", "ice"] let types3 = ["Fire", "grass", "rock", "grass"] let types = [types1, types2, types3].flatMap { $0 }
Collection同士に対するflatmap
処理にはjoined
処理が実行されるようになります。
そのため、結果が単純なArrayになります。
["electric", "electric", "fire", "water", "ice", "fire", "grass", "rock", "grass"]
Optionalに対するそれぞれの挙動
map
Optional
に対してもmap
とflatMap
が実装されています。
map
は中身が存在する(Some)場合、中身に対しての処理の結果を返すようになります。
let value: String? = "fire" let value1 = value.map { Type(rawValue: $0) }
この場合、処理内容によっては二重のOptionalになってしまうことがあります。
Optional(Optional(Type.fire))
flatMap
flatMap
の場合は中身に対しての処理の結果に対してアンラップを行うようになりますので、二重のOptionalを返すようなことは無くなります。
let value: String? = "fire" let value1 = value.flatMap { Type(rawValue: $0) }
Optional(Type.fire)
Optionalに対する操作を提供するということはOptional同士の処理を行う際に利用することもできるという事です。
例としてOptional同士の計算を行ってみます。
let value1: Int? = 10 let value2: Int? = 20 let value = value1.flatMap { v1 in value2.map { v2 in v1 + v2 } }
value1とvalue2がSomeの場合、それぞれ順番にflatMap
で中身を操作し、map
で開示してきた値を処理した結果を返せば以下のような結果を得る事が出来ます。
Optional(30)
ただし、この方法は複数の数値を扱う場合は煩雑になります。
4つの数値の計算の場合を例にしてみます。
let value1: Int? = 10 let value2: Int? = 20 let value3: Int? = 30 let value4: Int? = 40 let value = value1.flatMap { v1 in value2.flatMap { v2 in value3.flatMap { v3 in value4.map { v4 in v1 + v2 + v3 + v4 } } } }
Optional(100)
flatMap
を直列で処理したい場合にScalaとかではfor(yield)
のようなシンタックスシュガーが提供されていたりするのですが、Swiftは残念ながらその機構がまだありません。
その他には?
非同期処理を直列で行いたい場合にflatMap
を利用する事で簡潔に書く方法がありますが、こちらは外部ライブラリの助けを借りる必要があります。
この件に関しては こちらのブログで紹介したいと思います。
まとめ
map
とflatMap
の挙動の違いを書き記しました。
この内容に関しては勉強会などで断片的に発表してきたりしたのですが、説明不足や理解が足りない部分があったと思い、自分の復習も兼ねてまとめました。